package com.practice.chatroom;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URLEncoder;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.Scanner;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.math3.primes.Primes;
import org.json.JSONObject;

import com.practice.chatroom.Deal.HttpStatusCode;
import com.practice.chatroom.DAO;
import com.practice.chatroom.SessionBean;

/**
 * 本类专注于搭建聊天室webserver，并提供一些服务端全局方法
 */
public class ChatRoomServer {

    private ServerSocket serverSocket;
    /**
     * session池
     * 
     * @键 会话id
     * @值 会话bean
     * 
     * @see com.practice.chatroom.SessionBean
     */
    private Map<String, SessionBean> sessionPool = new ConcurrentHashMap<>();
    /**
     * openjfx gui客户端连接池
     * 
     * @键 用户名
     * @值 socket连接
     */
    private Map<String, Socket> clientPool = new ConcurrentHashMap<>();
    /**
     * 从数据库取出后，缓存的头像
     * 
     * @键 用户名
     * @值 头像图片base64串
     */
    private Map<String, String> userIcon = new HashMap<>();
    /**
     * RSA池，因为访问就随机生成个丢给其备用，定时才清除，固预留大一些<br/>
     * 
     * @键 模数
     * @值 公、私钥
     */
    private Map<BigInteger, BigInteger[]> RSApool = new ConcurrentHashMap<>(256);

    /** 全局自定义配置 */
    public JSONObject conf;
    /** 数据库访问对象 */
    public DAO dao = new DAO();
    /** 日志 */
    public Logger logger = Logger.getLogger("svrLogger");

    public static void main(String[] args) {
        new ChatRoomServer().start();
    }

    /** 开启服务端 */
    private void start() {
        try (FileInputStream fis = new FileInputStream(new File("./src/main/resources/conf.json"))) {
            conf = new JSONObject(new String(fis.readAllBytes())); // 读取配置文件
            // 设置日志级别
            logger.getParent().getHandlers()[0].setEncoding(System.getProperty("sun.jnu.encoding"));
            logger.setLevel(Level.parse(conf.getString("logLevel")));

            int port = conf.getInt("port");
            int maxConn = conf.getInt("maxConn");
            String host = conf.getString("host");
            this.serverSocket = new ServerSocket(port, maxConn, InetAddress.getByName(host));
            logger.info(String.format("服务端已开启 %s:%s 最大连接数：%s%n", host, port, maxConn));

            if (conf.getBoolean("withDB")) { // 连接数据库
                String driver = conf.getString("jdbcDriver");
                String conStr = conf.getString("conStr");
                String dbUser = conf.getString("dbUser");
                String pw = conf.getString("dbPassword");
                dao.connect(driver, conStr, dbUser, pw);
                logger.info(String.format("数据库已连接 驱动：%s 连接字符串：%s 用户名：%s 密码：%s%n", driver, conStr, dbUser, pw));
            }
            Executors.newSingleThreadScheduledExecutor().scheduleWithFixedDelay(() -> { // 开启暂离时限的周期性检查
                long now = System.currentTimeMillis();
                sessionPool.forEach((sessionId, sessionBean) -> {
                    if (sessionBean.getExpireTime() < now) { // 这个时间每次请求后会刷新
                        try {
                            offline(sessionId, "由于您过长时间未操作，即将自动下线！点击确定返回登录页", true);
                        } catch (AppException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }, 0, 10, TimeUnit.SECONDS); // 达到暂离限制则自动登出（大约每10秒检查）
            while (true) { // 开始服务，接收一切请求
                Socket socket = serverSocket.accept();
                logger.info(String.format("接收到 %s 发起的交互，开始处理%n", socket.getInetAddress().getHostAddress()));
                new Thread(new Deal(this, socket)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 新用户上线
     * 
     * @param username  用户名
     * @param icon      头像图片的base64串
     * @param RSAModule RSA模数，gui客户端时为空
     * @param socket    浏览器时为空，客户端时为长连的socket
     * @return 浏览器时返回当前登录的会话id，客户端时返回null
     * @exception AppException 可能会抛给前台的错误提示
     */
    public String onlineUser(String username, String icon, String RSAModule, Socket socket) throws AppException {
        if (Stream.concat(sessionPool.values().stream().map(SessionBean::getUsername), clientPool.keySet().stream())
                .anyMatch(s -> s.equals(username))) {
            throw new AppException("当前用户已在别处登录，不能重复操作，请先在别处下线！");
        }
        if (sessionPool.size() >= conf.getInt("maxConn")) {
            throw new AppException("已达访问上限！", HttpStatusCode.TOO_MANY_CONNECTIONS_421);
        }
        String sessionId = null;
        if (null == socket) { // 浏览器时
            sessionId = getNewSessionId();
            long now = System.currentTimeMillis();
            sessionPool.put(sessionId, new SessionBean(username, null, now + conf.getInt("afkLimit") * 1000, now)); // 缓存一份新session及相关信息
            RSApool.remove(new BigInteger(RSAModule)); // 登录后这对RSA就无用了
            logger.info(String.format("完成用户 %s 来自浏览器的登录，为其分配会话id：%s%n", username, sessionId));
        } else {
            clientPool.put(username, socket); // 缓存gui客户端的长连socket，此后交互都从这里取
            logger.info(String.format("完成用户 %s 来自gui客户端的登录%n", username));
        }
        userIcon.put(username, icon);
        return sessionId;
    }

    /**
     * 生成新的会话id
     * 
     * @return 随机session串
     */
    private String getNewSessionId() {
        Random random = new Random();
        Supplier<String> supplier = () -> IntStream.generate(() -> random.nextInt(61)).limit(conf.getInt("sessionLen"))
                .mapToObj(rnd -> { // 将随机数映射到3段ASCII上
                    if (rnd < 10) { // 数字
                        rnd += 48;
                    } else if (rnd > 35) { // 小写字母
                        rnd += 61;
                    } else { // 大写字母
                        rnd += 55;
                    }
                    return String.valueOf((char) rnd);
                }).collect(Collectors.joining()); // 按配置的位数随机生成0~9、A~Z、a~z三种范围的字符
        String newSessionId;
        while (sessionPool.containsKey((newSessionId = supplier.get()))) { // 反复生成直至不与已有的重复
        }
        return newSessionId;
    }

    /**
     * 验证会话id及用户是否存在且匹配
     * 
     * @param sessionId 会话id
     * @param username  用户名
     * @return 验证失败时返回null；成功时未超期返回原id，否则返回已自动更换的新id
     * @throws AppException 可能会抛给前台的错误提示
     */
    public String validateSession(String sessionId, String username) throws AppException {
        if (StringUtils.isEmpty(sessionId)) {
            return null;
        }
        if (sessionPool.containsKey(sessionId)) { // 会话id存在
            SessionBean session = sessionPool.get(sessionId);
            if (session.getUsername().equals(username)) { // 且用户名也匹配，通过验证
                long now = System.currentTimeMillis();
                session.setExpireTime(now + conf.getInt("afkLimit") * 1000); // 刷新暂离踢下线的时刻
                if (now - session.getSesseionBirth() > conf.getInt("sessionAge") * 1000) { // 超期了
                    sessionPool.remove(sessionId);
                    String newSessionId = getNewSessionId();
                    session.setSesseionBirth(now);
                    sessionPool.put(newSessionId, session); // 更换新session
                    logger.info(String.format("用户 %s 的session已从 %s 更换为 %s%n", username, sessionId, newSessionId));
                    sessionId = newSessionId;
                }
                return sessionId; // 没超期就返回原串
            }
        }
        offline(sessionId, null, true); // 验证失败也可能是会话id存在但用户不匹配，此时安全起见强制下线该会话
        return null;
    }

    /**
     * 解析参数串
     * 
     * @param param     参数字符串
     * @param separator 多对参数间的分隔符，如：&
     * @param assigner  单个参数的名、值间的赋值符，如：=
     * @return 解析为键值对后的map
     * @throws AppException 可能会抛给前台的错误提示
     */
    public Map<String, String> analysisStr(String param, String separator, String assigner) throws AppException {
        if (StringUtils.isEmpty(param)) {
            throw new AppException("参数为空！");
        }
        try {
            return Stream.of(param.trim().split(separator)).map(str -> {
                return str.split(assigner);
            }).collect(Collectors.toMap(str -> str[0].trim(), str -> str.length > 1 ? str[1].trim() : ""));
        } catch (Exception e) {
            e.printStackTrace();
            throw new AppException("解析请求参数错误：" + e.getMessage());
        }
    }

    /**
     * 建立WebSocket长连
     * 参见https://developer.mozilla.org/zh-CN/docs/Web/API/WebSockets_API/Writing_WebSocket_servers
     * 
     * @param sessionId 会话id
     * @param socket    发起这次升级请求的socket
     * @param wsKey     浏览器自动产生的随机钥
     */
    public void buildWS(String sessionId, Socket socket, String wsKey) {
        String ws = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept:"
                + Base64.encodeBase64String(DigestUtils.sha1(wsKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
                + "\r\n\r\n"; // 将随机钥与规范中的固定“魔法值”按规则计算，浏览器将检查结果是否匹配
        try {
            OutputStream os = socket.getOutputStream();
            os.write(ws.getBytes());
            os.flush(); // 完成首次握手
            logger.info(String.format("会话 %s 发起websocket连接，服务端回应：%n%s%n", sessionId, ws));
            sessionPool.get(sessionId).setWsSocket(socket); // 在session池中存储socket，之后就可以随时推送消息了
            pushOnlineUser(); // ws建立后就立即做首次推送，因登录后便需先获取次在线用户信息
            try (Scanner sc = new Scanner(socket.getInputStream())) {
                while (sc.hasNextLine()) { // ws本身是支持双向的，但目前本应用中前端尚无这类传讯
                    logger.info(sc.nextLine()); // 一方面传统请求够用，另外ws中b to s是强制加密，懒得处理
                }
            }
            logger.info(String.format("会话 %s 的ws已意外断开，可能其直接关闭了浏览器，服务端将做登出处理%n", sessionId));
            offline(sessionId, null, true); // 利用scanner对socket输入流的阻塞式等待，走到这句即说明连接断开，可能有直接关浏览器等情况，需强制下线
        } catch (IOException | AppException e) { // 目前暂未设计对ws传输中的错误，做前端的接收提示
            e.printStackTrace();
        }
    }

    /**
     * 服务端主动向浏览器推送数据 参见https://tools.ietf.org/html/rfc6455#section-5.1
     * 
     * <pre>
     *   0                   1                   2                   3
     * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     *+-+-+-+-+-------+-+-------------+-------------------------------+
     *|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
     *|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
     *|N|V|V|V|       |S|             |   (if payload len==126/127)   |
     *| |1|2|3|       |K|             |                               |
     *+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
     *|     Extended payload length continued, if payload len == 127  |
     *+ - - - - - - - - - - - - - - - +-------------------------------+
     *|                               |Masking-key, if MASK set to 1  |
     *+-------------------------------+-------------------------------+
     *| Masking-key (continued)       |          Payload Data         |
     *+-------------------------------- - - - - - - - - - - - - - - - +
     *:                     Payload Data continued ...                :
     *+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
     *|                     Payload Data continued ...                |
     *+---------------------------------------------------------------+
     * </pre>
     * 
     * @param sessionId 要推送的会话
     * @param data      要发送的数据，应为json串
     */
    public void pushWS(String sessionId, String data) {
        try {
            OutputStream os = sessionPool.get(sessionId).getWsSocket().getOutputStream();
            byte[] bytes = ("(" + data + ")").getBytes("UTF-8");
            os.write((byte) -127); // 本应用开头8位根据规范固定写最常见的 1000 0001
            long len = bytes.length;
            if (len < 126) { // 数据长度小于126时，报头第二个字节（即9~16位），直接写入长度的二进制，第三个字节就开始传数据了
                os.write((byte) len);
            } else if (len < Math.pow(2, 16)) { // 数据长度介于[126,65535]时，第二字节写126，三、四字节（即17~32位）写长度，第五字节开始传数据
                os.write((byte) 126);
                os.write((byte) (len >> 8));
                os.write((byte) (len & 255));
            } else { // 数据长度超过65535时，第二字节写127，三~十节（即17~80位）写长度，第十一字节开始传数据
                os.write((byte) 127);
                for (int i = 56; i >= 0; i -= 8) { // 写长度的二进制的第i+8~第i位
                    os.write((byte) ((len >> i) & 255)); // 先通过右移使本次写入的8位区段移至0~7位，再逻辑与255，抛弃掉8位及更高的
                }
            }
            os.write(bytes);
            os.flush();
            logger.info(String.format("对会话 %s 以ws推送了数据：%s%n", sessionId, data));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 与gui客户端交互
     * 
     * @param username 已从gui客户端登录的用户名
     * @param data     要发送的数据，为方便也是json串
     * @throws IOException socket可能已关闭的异常
     */
    public void writeToClient(String username, String data) throws IOException {
        OutputStream socketOS = clientPool.get(username).getOutputStream();
        writeToClient(socketOS, data);
        logger.info(String.format("对gui客户端用户 %s 发送了数据：%s%n", username, data));
    }

    /**
     * 与gui客户端交互
     * 
     * @param socketOS 已与gui客户端建立的连接
     * @param data     要发送的数据，为方便也是json串
     * @throws IOException socket可能已关闭的异常
     */
    public void writeToClient(OutputStream socketOS, String data) throws IOException {
        socketOS.write((data + "\r\n").getBytes("UTF-8")); // 约定每次发2行，首行为通信类型，第二行为数据
        socketOS.flush();
    }

    /**
     * 用户下线
     * 
     * @param identity    浏览器时为会话id，gui客户端时为用户名
     * @param note        若为服务端主动对其下线，需要提示的消息，可以不传
     * @param fromBroswer 是否为浏览器登录的用户
     * @throws AppException 可能会抛给前台的错误提示
     */
    public void offline(String identity, String note, boolean fromBroswer) throws AppException {
        try {
            String username = null;
            if (fromBroswer && sessionPool.containsKey(identity)) { // 浏览器时
                SessionBean session = sessionPool.get(identity);
                username = session.getUsername();
                Socket wsSocket = session.getWsSocket();
                if (!wsSocket.isClosed()) { // 只要不是直接关浏览器应该都还连着
                    pushWS(identity, "{msgType:'location',datas:{url:'/'"
                            + (StringUtils.isEmpty(note) ? "" : ",note:'" + note + "'") + "}}");
                    wsSocket.close(); // 关闭ws
                }
                sessionPool.remove(identity); // 移除session
                logger.info(String.format("会话 %s 完成下线，移除了session关闭了ws%n", identity));
                pushOnlineUser(); // 通知其他用户
            }
            if (!fromBroswer && clientPool.containsKey(identity)) { // gui客户端时
                clientPool.get(identity).close();
                clientPool.remove(identity);
                logger.info(String.format("gui客户端用户 %s 完成下线%n", identity));
                pushOnlineUser(); // 通知其他用户
            }
            if (conf.getBoolean("withDB") && null != username) { // 数据库更新上次登出时间
                Class<?>[] colTypes = new Class<?>[] { Timestamp.class, String.class };
                String[] colNames = "last_out,username".split(",");
                Map<String, Object> map = new HashMap<>();
                Timestamp now = new Timestamp(System.currentTimeMillis());
                map.put(colNames[0], now);
                map.put(colNames[1], username);
                String sql = "UPDATE chat_room_user SET last_out = ? WHERE username = ?;";
                dao.update(sql, map, colNames, colTypes);
                logger.info(String.format("数据库更新用户 %s 的上次登出时间为 %s%n", username, now));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /** 推送当前在线用户 */
    public void pushOnlineUser() {
        Map<String, String> onlineUsers = sessionPool.values().stream()
                .collect(Collectors.toMap(SessionBean::getUsername, session -> "{username:'" + session.getUsername()
                        + "',icon:'" + userIcon.get(session.getUsername()) + "',isSelf:false}")); // 先以每个用户初始化一个JSON串
        onlineUsers.putAll(clientPool.keySet().stream().collect(Collectors.toMap(String::new,
                username -> "{username:'" + username + "',icon:'" + userIcon.get(username) + "',isSelf:false}"))); // 追加客户端在线用户
        logger.info(String.format("开始对所有在线用户推送最新的当前在线用户情况 数量：%s%n", onlineUsers.size()));

        sessionPool.entrySet().forEach(poolEntry -> { // 再遍历当前session池，将初始化的多份JSON聚集为数组串，其中与当前session用户一致的，修改isSelf为true，然后推送
            String jsonStr = onlineUsers.entrySet().stream().map(entry -> {
                String json = entry.getValue();
                return (poolEntry.getValue().getUsername().equals(entry.getKey())
                        ? json.replace("isSelf:false", "isSelf:true")
                        : json);
            }).collect(Collectors.joining(","));
            pushWS(poolEntry.getKey(), "{msgType:'refreshOnlineUser',datas:[" + jsonStr + "]}");
        });

        try { // 遍历客户端池推送
            String json = "{msgType:'refreshOnlineUser',datas:["
                    + onlineUsers.values().stream().collect(Collectors.joining(",")) + "]}";
            for (String username : clientPool.keySet()) {
                writeToClient(username, json);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 推送新聊天消息
     * 
     * @param username 发出消息的用户名
     * @param isImg    是否发的图片
     * @param msg      消息内容，图片时为base64串
     * @param color    浏览器端支持自定义字色
     * @param fontSize 浏览器端支持自定义字号
     */
    public void spreadMsg(String username, boolean isImg, String msg, String color, String fontSize) {
        String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
        String dataPreStr = "{msgType:'spreadMsg',datas:{username:'" + username + "',icon:'" + userIcon.get(username)
                + "',fontSize:'" + fontSize + (isImg ? "',isImg:true" : "',color:'" + color + "',isImg:false")
                + ",timestamp:'" + now + "',msg:'"; // 先初始化JSON串前面大部分
        logger.info(String.format("开始对所有在线用户推送新聊天消息 发起人：%s 是否图片:%s%n", username, isImg));

        sessionPool.entrySet().forEach(entry -> { // 再遍历当前session池群发，其中消息来源用户isSelf为true
            pushWS(entry.getKey(), dataPreStr + msg + "',isSelf:"
                    + (entry.getValue().getUsername().equals(username) ? "true" : "false") + "}}");
        });

        String clientStr;
        try {
            clientStr = dataPreStr + URLEncoder.encode(msg, "UTF-8") + "'}}";
            clientPool.keySet().forEach(user -> { // 遍历客户端池
                try {
                    writeToClient(user, clientStr);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    /**
     * 生成一组新RSA 参见维基百科：
     * 
     * @RSA加密算法 https://en.wikipedia.org/wiki/RSA_(cryptosystem)
     * @模逆元-数论倒数 https://en.wikipedia.org/wiki/Modular_multiplicative_inverse
     * @中国剩余定理 https://en.wikipedia.org/wiki/Chinese_remainder_theorem
     * 
     * @return 返回模及公钥，服务端自己缓存私钥
     */
    public BigInteger[] getNewRSA() {
        Random rnd = new Random(); // 随机位数更难破解，且原文必须小于模，所以磨越大越好
        int half = (int) Math.sqrt(Integer.MAX_VALUE); // 随机位数更难破解，范围int上限至其平方根，既不超apache包生成素数的上限，又可保证素数积至少有MAX_VALUE那么大
        Supplier<Integer> rndPrime = () -> Primes.nextPrime(half + rnd.nextInt(half)); // 原文必须小于模，所以越大越好，若还是超了需分段
        int pP; // 大素数p
        int pQ; // 大素数q
        BigInteger modNum = BigInteger.ZERO; // 即维基百科中的N，该模会保留。考虑到后面指数运算或哪天扩展到生产级别位数，直接使用BigInteger
        do { // 尽管重复的几率非常低，还是以防万一循环生成。p、q生成好公私钥后则会抛弃，这也是RSA的安全核心
            pP = rndPrime.get(); // 因为在传讯中无需再使用p、q，但抛弃后，仅靠公钥和模，却极难回算它们，也就极难推出私钥
            pQ = rndPrime.get(); // 本例因Primes库限制，模数N最大为int上限平方，即long范围内
            modNum = BigInteger.valueOf(pP).multiply(BigInteger.valueOf(pQ)); // 其实BigInteger似乎有方法测定一个大数是否“很可能”为素数，暂时没研究
        } while (RSApool.containsKey(modNum)); // 实际以当下技术的运算能力，1024位才保险，推荐2048位。当然量子计算机若实现，由N分解pq什么的就是渣渣了
        BigInteger lcd[] = extendedEuclideanAlgorithm(BigInteger.valueOf(pP - 1), BigInteger.valueOf(pQ - 1)); // 原理详见该方法注释
        BigInteger lcm = BigInteger.valueOf(pP - 1).multiply(BigInteger.valueOf(pQ - 1)).divide(lcd[2]); // 最小公倍数，维基百科中的r
        BigInteger pub; // 与r互质的适当数
        BigInteger[] keyNums;
        do { // 随出维基百科中的e，并求到用于构建d的模逆元通解
            pub = lcm.multiply(BigInteger.valueOf((long) ((rnd.nextDouble() * 0.5 + 0.1) * Long.MAX_VALUE)))
                    .divide(BigInteger.valueOf(Long.MAX_VALUE));
            keyNums = extendedEuclideanAlgorithm(lcm, pub);
        } while (keyNums[2].compareTo(BigInteger.ONE) != 0); // 即 keyNums[1]*e + keyNums[0]*lcm mod r = 1
        BigInteger priv = keyNums[1]; // 维基百科中的d
        while (priv.compareTo(BigInteger.ZERO) < 0) { // 而构建通解的模r的系数，即r的整倍数在mod r时都可被忽略掉
            priv = priv.add(lcm); // 换言之，用不定方程ax+by=1的通解算一个最小正x，因为x、y通常为一正一负
        }
        while (priv.compareTo(lcm) > 0) { // 在RSA中，对应维基百科里ed+kr=1，k取一个能构建小正d的值
            priv = priv.subtract(lcm);
        }
        RSApool.put(modNum, new BigInteger[] { pub, priv }); // 此时便生成好了公钥e及私钥r，注意模也是双方都要用到的
        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
        BigInteger RSAmodule = modNum;
        scheduler.schedule(() -> {
            if (RSApool.containsKey(RSAmodule)) { // 若已经注册或登录，可能已经移除了
                RSApool.remove(RSAmodule);
            }
        }, conf.getInt("RSAage"), TimeUnit.SECONDS); // 时限设长点，且cookie一致，可防止反复访问，推测RSA位数
        logger.info(String.format("成功生成新RSA 原质因子：%s %s 模数：%s 公钥:%s 私钥：%s%n", pP, pQ, modNum, pub, priv));
        return new BigInteger[] { modNum, priv };
    }

    /**
     * 欧几里得扩展算法 参见https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm
     * 
     * @param dividend 被除数
     * @param divisor  除数，求模逆元时应为模数
     * @return 不定方程的被除数系数、除数系数、最大公约数 即ax+by=lcd中的x、y、lcd
     */
    private BigInteger[] extendedEuclideanAlgorithm(BigInteger dividend, BigInteger divisor) {
        if (dividend.compareTo(divisor) < 0) { // 这里为减少烧脑的内容，RSA时还需求的最小公倍数及互质也都借用本方法，不再额外单独求
            BigInteger tmp = dividend; // 因为对任意两数a、b，其最小公倍数lcm，可用二者之积除以最大公约数lcd得到
            dividend = divisor; // 而互质时lcd必然为1
            divisor = tmp; // 欧几里得辗转相除法原本仅用于求最大公约数
        }
        BigInteger[] quoAndrem = dividend.divideAndRemainder(divisor); // 扩展后可求ax+by=lcd的不定方程通解
        if (quoAndrem[1].compareTo(BigInteger.ZERO) != 0) { // 且a、b互质lcd必为1，则必存在某个解x，其为a关于y的模逆元（数论倒数），即a*x与1对b同余（因为b*y在modb时显然会被抵消）
            BigInteger[] recursion = extendedEuclideanAlgorithm(divisor, quoAndrem[1]); // 此时求得的x，有形式x+kb均为a关于b的模逆元通解
            return new BigInteger[] { recursion[1], recursion[0].subtract(recursion[1].multiply(quoAndrem[0])),
                    recursion[2] }; // 当不定方程的a为RSA中的lcd(p-1,q-1)、b为选择出与a互质的恰当公钥时，就能用模逆元构造私钥
        } else { // 即a、b为维基百科中介绍RSA过程时所说的r、e，用本法则可求d
            return new BigInteger[] { BigInteger.ZERO, BigInteger.ONE, divisor }; // 本身求不定方程解的原理，是收集辗转相除过程中的因子（商）并迭代返还，参见“中国剩余定理”
        }
    }

    /**
     * 根据传来的模与密文，取到缓存的公钥，算出原文返回
     * 
     * @param RSAmodule 模数
     * @param cipher    单段密文
     * @return 单段原文
     * @throws AppException 可能会抛给前台的错误提示
     */
    public BigInteger decodeRSA(BigInteger RSAmodule, BigInteger cipher) throws AppException {
        if (!RSApool.containsKey(RSAmodule)) {
            throw new AppException("密文或相关信息不正常，操作失败！");
        }
        return cipher.modPow(RSApool.get(RSAmodule)[0], RSAmodule);
    }

    /**
     * 得到当前服务端地址
     * 
     * @return 返回当前服务端socket绑定的ip
     */
    public String getHost() {
        return serverSocket.getInetAddress().getHostAddress();
    }
}